package mutation

import (
	"context"
	"fmt"
	"github.com/InVisionApp/conjungo"
	"github.com/Masterminds/sprig/v3"
	"github.com/nuttech/bell"
	"github.com/wI2L/jsondiff"
	"gitverse.ru/synapse/kubelatte/pkg/api/common"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/observability/logs"
	"gitverse.ru/synapse/kubelatte/pkg/storage"
	"gitverse.ru/synapse/kubelatte/pkg/util"
	"gitverse.ru/synapse/kubelatte/pkg/util/templates"
	"go.uber.org/zap"
	"html/template"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/yaml"
	"log"
	"reflect"
	"strings"
	"testing"
)

func TestMutateController_CheckMatchSelector(t *testing.T) {
	type args struct {
		config *v1alpha1.MutationConfig
		obj    objInfo
	}
	tests := []struct {
		name string
		args args
		want bool
	}{
		{
			name: "wrong operator",
			args: args{
				config: &v1alpha1.MutationConfig{
					LabelSelector: metav1.LabelSelector{
						MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "", Values: []string{}, Operator: "s0m3 wr0n9 0p3rat0r"}}},
				},
				obj: objInfo{
					labels: map[string]string{"str": "str"},
				},
			},
			want: false,
		},
		{
			name: "matching",
			args: args{
				config: &v1alpha1.MutationConfig{
					ObjectSelector: v1alpha1.ObjectSelector{Kind: "pod", Name: "name"},
					LabelSelector: metav1.LabelSelector{
						MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "", Values: []string{}, Operator: "s0m3 wr0n9 0p3rat0r"}}},
				},
				obj: objInfo{
					kind:   "pod",
					name:   "name",
					labels: map[string]string{"str": "str"},
				},
			},
			want: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			logs.Logger = zap.NewNop()

			r := &Controller{}
			if got := r.checkMatchSelector(context.Background(), tt.args.config, tt.args.obj); got != tt.want {
				t.Errorf("checkMatchSelector() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestMutateController_GetRenders(t *testing.T) {
	tests := []struct {
		name     string
		obj      map[string]interface{}
		trigger  v1alpha1.Trigger
		template v1alpha1.Template
		want     []util.RenderItem
	}{
		{
			name: "merge",
			obj: map[string]interface{}{
				"metadata": map[string]interface{}{
					"annotations": map[string]interface{}{
						"ns/annotK": "enabled",
					},
					"labels": map[string]interface{}{
						"labK": "labV",
					},
				},
			},
			trigger: v1alpha1.Trigger{
				TypeMeta:   metav1.TypeMeta{},
				ObjectMeta: metav1.ObjectMeta{},
				Spec: v1alpha1.TriggerSpec{
					MutationConfigs: []v1alpha1.MutationConfig{{
						UpdateStrategy:      "",
						AnnotationNamespace: "ns",
						AnnotationTrigger:   "annotK",
						TemplateRefs:        []string{"templNs/templName"},
						LabelSelector: metav1.LabelSelector{
							MatchLabels: nil,
							MatchExpressions: []metav1.LabelSelectorRequirement{
								{
									Key:      "labK",
									Operator: "Exists",
									Values:   nil,
								},
							},
						},
					}}},
				Status: v1alpha1.TriggerStatus{},
			},
			template: v1alpha1.Template{
				TypeMeta: metav1.TypeMeta{},
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "templNs",
					Name:      "templName",
				},
				Spec:   v1alpha1.TemplateSpec{},
				Status: v1alpha1.TemplateStatus{},
			},
			want: []util.RenderItem{{
				Template: v1alpha1.Template{
					TypeMeta: metav1.TypeMeta{},
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "templNs",
						Name:      "templName",
					},
					Spec:   v1alpha1.TemplateSpec{},
					Status: v1alpha1.TemplateStatus{},
				},
				Render: "",
				Action: "",
			},
			},
		},
		{
			name: "replace",
			obj: map[string]interface{}{
				"metadata": map[string]interface{}{
					"annotations": map[string]interface{}{
						"ns/annotK": "enabled",
					},
					"labels": map[string]interface{}{
						"labK": "labV",
					},
				},
			},
			trigger: v1alpha1.Trigger{
				TypeMeta:   metav1.TypeMeta{},
				ObjectMeta: metav1.ObjectMeta{},
				Spec: v1alpha1.TriggerSpec{
					MutationConfigs: []v1alpha1.MutationConfig{{
						Name:                "",
						UpdateStrategy:      "replace",
						AnnotationNamespace: "ns",
						AnnotationTrigger:   "annotK",
						TemplateRefs:        []string{"templNs/templName"},
						LabelSelector: metav1.LabelSelector{
							MatchLabels: nil,
							MatchExpressions: []metav1.LabelSelectorRequirement{
								{
									Key:      "labK",
									Operator: "Exists",
									Values:   nil,
								},
							},
						},
					}}},
				Status: v1alpha1.TriggerStatus{},
			},
			template: v1alpha1.Template{
				TypeMeta: metav1.TypeMeta{},
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "templNs",
					Name:      "templName",
				},
				Spec:   v1alpha1.TemplateSpec{},
				Status: v1alpha1.TemplateStatus{},
			},
			want: []util.RenderItem{{
				Template: v1alpha1.Template{
					TypeMeta: metav1.TypeMeta{},
					ObjectMeta: metav1.ObjectMeta{
						Namespace: "templNs",
						Name:      "templName",
					},
					Spec:   v1alpha1.TemplateSpec{},
					Status: v1alpha1.TemplateStatus{},
				},
				Render: "",
				Action: "replace",
			},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			logs.Logger = zap.NewNop()
			r := &Controller{}
			storage.Storage = &storage.StorageController{}
			storage.Storage.Start(false, false)
			storage.Storage.UpdateTrigger(bell.Message{Value: tt.trigger})
			storage.Storage.UpdateTemplate(bell.Message{Value: tt.template})

			if got := r.getRenders(context.Background(), &common.ARFields{Object: tt.obj}); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("getRenders() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestMutateController_Mutate(t *testing.T) {
	type args struct {
		obj map[string]interface{}
		raw []byte
	}
	tests := []struct {
		name     string
		trigger  v1alpha1.Trigger
		template v1alpha1.Template
		args     args
		want     []jsondiff.Operation
	}{
		{
			name: "ok",
			want: []jsondiff.Operation{
				{
					Value:    nil,
					OldValue: "annotV",
					Type:     "remove",
					From:     "",
					Path:     "/metadata/annotations/annotK",
				},
				{
					Value:    "annotVal",
					OldValue: nil,
					Type:     "add",
					From:     "",
					Path:     "/metadata/annotations/annotKey",
				},
				{
					Value:    nil,
					OldValue: map[string]interface{}{"labK": "labV"},
					Type:     "remove",
					From:     "",
					Path:     "/metadata/labels",
				},
			},
			args: args{
				obj: map[string]interface{}{
					"metadata": map[string]interface{}{
						"annotations": map[string]interface{}{
							"annotK": "annotV",
						},
						"labels": map[string]interface{}{
							"labK": "labV",
						},
					},
				},
				raw: []byte("{\n  \"metadata\": {\n    \"annotations\": {\n      \"annotKey\": \"annotVal\"\n    }\n  }\n}"),
			},
			trigger: v1alpha1.Trigger{
				TypeMeta:   metav1.TypeMeta{},
				ObjectMeta: metav1.ObjectMeta{},
				Spec: v1alpha1.TriggerSpec{
					MutationConfigs: []v1alpha1.MutationConfig{{
						Name:           "",
						UpdateStrategy: "",
						TemplateRefs:   []string{"templNs/templName"},
						LabelSelector: metav1.LabelSelector{
							MatchLabels: nil,
							MatchExpressions: []metav1.LabelSelectorRequirement{
								{
									Key:      "labK",
									Operator: "Exists",
									Values:   nil,
								},
							},
						},
					}}},
				Status: v1alpha1.TriggerStatus{},
			},
			template: v1alpha1.Template{
				TypeMeta: metav1.TypeMeta{},
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "templNs",
					Name:      "templName",
				},
				Spec:   v1alpha1.TemplateSpec{},
				Status: v1alpha1.TemplateStatus{},
			},
		},
		{
			name: "ok",
			args: args{obj: map[string]interface{}{
				"kind": "Pod",
				"metadata": map[string]interface{}{
					"annotations": map[string]interface{}{
						"ann_ns/ann_trig": "enabled",
					},
				},
			},
				raw: []byte("{\"spec\": {\"containers\": [{\"name\": \"cont1\",\"image\": \"noimage\"}]}}")},
			trigger: v1alpha1.Trigger{
				Spec: v1alpha1.TriggerSpec{
					MutationConfigs: []v1alpha1.MutationConfig{
						{
							AnnotationNamespace: "ann_ns",
							AnnotationTrigger:   "ann_trig",
							Name:                "mutation cfg",
							UpdateStrategy:      "merge",
							Containers:          []string{"some-ns/some-templ/container_mutated"}},
					},
				},
			},

			template: v1alpha1.Template{
				ObjectMeta: metav1.ObjectMeta{Namespace: "some-ns", Name: "some-templ"},
				Spec: v1alpha1.TemplateSpec{Data: `spec:
  containers:
    - name: container_mutated
      image: imagemut`,
				},
			},
			want: []jsondiff.Operation{
				{Type: "remove", Path: "/kind", OldValue: "Pod"},
				{Type: "remove", Path: "/metadata/annotations", OldValue: map[string]interface{}{"ann_ns/ann_trig": "enabled"}},
				{Value: map[string]interface{}{"kblt.mutation.resource": "true"}, Type: "add", Path: "/metadata/labels"},
				{Value: map[string]interface{}{
					"containers": []interface{}{
						map[string]interface{}{"image": "noimage", "name": "cont1"},
						map[string]interface{}{"image": "imagemut", "name": "container_mutated"},
					},
				}, Type: "add", Path: "/spec"},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			logs.Logger = zap.NewNop()
			r := &Controller{}
			storage.Storage = &storage.StorageController{}
			storage.Storage.Start(false, false)
			storage.Storage.UpdateTrigger(bell.Message{Value: tt.trigger})
			storage.Storage.UpdateTemplate(bell.Message{Value: tt.template})

			if got := r.ApplyMutation(context.Background(), &common.ARFields{Object: tt.args.obj}, tt.args.raw); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Mutate() = %v, want %v\n", got, tt.want)
				for i := 0; i < len(got); i++ {
					fmt.Printf("%v: result: %+v\n", i, got[i])
					fmt.Printf("%v: want  : %+v\n", i, tt.want[i])
				}
			}
		})
	}
}

func TestMergeFunc(t *testing.T) {
	ym1 := "containers: \n - name: test\n   image: aaaaaa\n   testdt:\n    - asda\n    - adad\n    - cvbcvb\n - name: test1\n   image: bbbbbb\n - name: test2\n   image: cccccc    "
	ym2 := "containers: \n - name: test\n   image: ssssss\n   testdt:\n    - xxx\n    - yyy\n    - zzz\n - name: test2\n   image: zzzzzz    "
	var obj1 map[string]interface{}
	var obj2 map[string]interface{}
	yaml.Unmarshal([]byte(ym1), &obj1)
	yaml.Unmarshal([]byte(ym2), &obj2)
	opts := conjungo.NewOptions()
	opts.SetKindMergeFunc(
		reflect.Slice,

		func(t, s reflect.Value, o *conjungo.Options) (reflect.Value, error) {
			aT, _ := t.Interface().([]interface{})
			aS, _ := s.Interface().([]interface{})

			aT = util.MergeSlice(aT, aS)

			return reflect.ValueOf(aT), nil
		},
	)
	opts.Overwrite = false
	err := conjungo.Merge(&obj1, obj2, opts)
	if err != nil {
		fmt.Printf("%s", err)
	}
	fmt.Printf("Data %v\n", obj1)
}

func TestRangeTemplate(t *testing.T) {
	data := "spec:\n      template:\n          spec:\n            containers:\n            {{% range .spec.template.spec.containers %}}\n              - name: {{% .name %}}\n                securityContext:\n                  privileged: false\n                  runAsUser: 10000\n                  runAsGroup: 5000\n                  readOnlyRootFilesystem: true\n                  allowPrivilegeEscalation: false\n                  capabilities:\n                    drop:\n                      - ALL       \n            {{% end %}}"
	templateEngine := template.New("common").
		Delims("{{%", "%}}").
		Funcs(template.FuncMap{
			"fromYaml": templates.FromYAML,
		}).
		Funcs(sprig.TxtFuncMap())
	temp, err := templateEngine.New("test").Parse(data)
	if err != nil {
		log.Fatalf("Failed parse %s", err)
	}

	var inputObj interface{}
	input := "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: test-igeg\n  namespace: synapse-metrics-system\n  annotations:\n    kblt.synapse/deploymentigeg: enabled\n    kblt.synapse/deployment-all: enabled\n  labels:\n    secman-injector: enabled\n    user-label: 'true'\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: egw-syn-metrics-synapse-metrics-system\n      istio: egw-syn-metrics-synapse-metrics-system\n  template:\n    metadata:\n      labels:\n        app: egw-syn-metrics-synapse-metrics-system\n        istio: egw-syn-metrics-synapse-metrics-system\n      annotations:\n        openshift.io/scc: restricted\n        sidecar.istio.io/inject: 'false'\n    spec:\n      volumes:\n        - configMap:\n            defaultMode: 256\n            name: istio-basic\n            optional: true\n          name: config-volume\n        - name: istiod-ca-cert\n          configMap:\n            name: istio-ca-root-cert\n            defaultMode: 256\n        - name: podinfo\n          downwardAPI:\n            items:\n              - path: labels\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: metadata.labels\n              - path: annotations\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: metadata.annotations\n            defaultMode: 256\n        - name: istio-envoy\n          emptyDir: {}\n        - name: istio-data\n          emptyDir: {}\n        - name: istio-token\n          projected:\n            sources:\n              - serviceAccountToken:\n                  audience: istio-ca\n                  expirationSeconds: 43200\n                  path: istio-token\n            defaultMode: 256\n      containers:\n        - name: istio-proxy\n          image: image\n          args:\n            - proxy\n            - router\n            - '--domain'\n            - $(POD_NAMESPACE).svc.cluster.local\n            - '--proxyLogLevel=warning'\n            - '--proxyComponentLogLevel=misc:error'\n            - '--log_output_level=default:info'\n            - '--serviceCluster'\n            - $(POD_NAMESPACE)\n            - '--trust-domain=cluster.local'\n          ports:\n            - name: status-port\n              containerPort: 15021\n              protocol: TCP\n            - name: https-kubeapi\n              containerPort: 4443\n              protocol: TCP\n          env:\n            - name: JWT_POLICY\n              value: first-party-jwt\n            - name: TRUST_DOMAIN\n              value: cluster.local\n            - name: ISTIO_META_UNPRIVILEGED_POD\n              value: 'true'\n            - name: PILOT_CERT_PROVIDER\n              value: istiod\n            - name: CA_ADDR\n              value: blabla.ci00000-cp.svc:15012\n            - name: NODE_NAME\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: spec.nodeName\n            - name: POD_NAME\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: metadata.name\n            - name: POD_NAMESPACE\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: metadata.namespace\n            - name: INSTANCE_IP\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: status.podIP\n            - name: HOST_IP\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: status.hostIP\n            - name: SERVICE_ACCOUNT\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: spec.serviceAccountName\n            - name: CANONICAL_SERVICE\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: metadata.labels['service.istio.io/canonical-name']\n            - name: CANONICAL_REVISION\n              valueFrom:\n                fieldRef:\n                  apiVersion: v1\n                  fieldPath: metadata.labels['service.istio.io/canonical-revision']\n            - name: ISTIO_META_WORKLOAD_NAME\n              value: egressgateway-synapse-metrics\n            - name: ISTIO_META_OWNER\n              value: >-\n                kubernetes://apis/apps/v1/namespaces/synapse-metrics-system/deployments/egw-syn-metrics-synapse-metrics-system\n            - name: ISTIO_META_MESH_ID\n              value: cluster.local\n            - name: ISTIO_META_ROUTER_MODE\n              value: sni-dnat\n            - name: ISTIO_META_CLUSTER_ID\n              value: Kubernetes\n          resources:\n            limits:\n              cpu: 300m\n              ephemeral-storage: 500Mi\n              memory: 500Mi\n            requests:\n              cpu: 300m\n              ephemeral-storage: 500Mi\n              memory: 500Mi\n          volumeMounts:\n            - mountPath: /etc/istio/config\n              name: config-volume\n            - name: istio-envoy\n              mountPath: /etc/istio/proxy\n            - name: istiod-ca-cert\n              mountPath: /var/run/secrets/istio\n            - name: podinfo\n              mountPath: /etc/istio/pod\n            - name: istio-token\n              readOnly: true\n              mountPath: /var/run/secrets/tokens\n            - name: istio-data\n              mountPath: /var/lib/istio/data\n          readinessProbe:\n            httpGet:\n              path: /healthz/ready\n              port: 15021\n              scheme: HTTP\n            initialDelaySeconds: 1\n            timeoutSeconds: 1\n            periodSeconds: 2\n            successThreshold: 1\n            failureThreshold: 30\n          terminationMessagePath: /dev/termination-log\n          terminationMessagePolicy: File\n          imagePullPolicy: Always\n          securityContext:\n            readOnlyRootFilesystem: true\n      restartPolicy: Always\n      terminationGracePeriodSeconds: 30\n      dnsPolicy: ClusterFirst\n      affinity:\n        nodeAffinity:\n          requiredDuringSchedulingIgnoredDuringExecution:\n            nodeSelectorTerms:\n              - matchExpressions:\n                  - key: kubernetes.io/arch\n                    operator: In\n                    values:\n                      - amd64\n                      - ppc64le\n                      - s390x\n          preferredDuringSchedulingIgnoredDuringExecution:\n            - weight: 2\n              preference:\n                matchExpressions:\n                  - key: kubernetes.io/arch\n                    operator: In\n                    values:\n                      - amd64\n            - weight: 2\n              preference:\n                matchExpressions:\n                  - key: kubernetes.io/arch\n                    operator: In\n                    values:\n                      - ppc64le\n            - weight: 2\n              preference:\n                matchExpressions:\n                  - key: kubernetes.io/arch\n                    operator: In\n                    values:\n                      - s390x\n      schedulerName: default-scheduler\n  strategy:\n    type: RollingUpdate\n    rollingUpdate:\n      maxUnavailable: 25%\n      maxSurge: 100%\n  revisionHistoryLimit: 10\n  progressDeadlineSeconds: 600"
	err = yaml.Unmarshal([]byte(input), &inputObj)
	if err != nil {
		log.Fatalf("Failed Unmarshal %s", err)
	}

	var tempBuffer strings.Builder
	err = temp.Execute(&tempBuffer, inputObj)
	if err != nil {
		log.Fatalf("Failed Execute %s", err)
	}

	log.Printf("%s", tempBuffer.String())
}
